﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Net.Http;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Http.Resilience;
using Microsoft.Extensions.Http.Resilience.Hedging.Internals;
using Microsoft.Extensions.Http.Resilience.Internal;
using Microsoft.Extensions.Http.Resilience.Internal.Validators;
using Microsoft.Extensions.Http.Resilience.Routing.Internal;
using Microsoft.Shared.Diagnostics;
using Polly;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extensions for <see cref="IHttpClientBuilder"/>.
/// </summary>
public static partial class ResilienceHttpClientBuilderExtensions
{
    /// <summary>
    /// Adds a standard hedging handler that wraps the execution of the request with a standard hedging mechanism.
    /// </summary>
    /// <param name="builder">The HTTP client builder.</param>
    /// <param name="configure">Configures the routing strategy associated with this handler.</param>
    /// <returns>
    /// A <see cref="IStandardHedgingHandlerBuilder"/> instance that can be used to configure the standard hedging behavior.
    /// </returns>
    /// <remarks>
    /// The standard hedging uses a pool of circuit breakers to ensure that unhealthy endpoints are not hedged against.
    /// By default, the selection from pool is based on the URL Authority (scheme + host + port).
    /// It is recommended that you configure the way the strategies are selected by calling
    /// <see cref="StandardHedgingHandlerBuilderExtensions.SelectPipelineByAuthority(IStandardHedgingHandlerBuilder)"/>
    /// extensions.
    /// <para>
    /// See <see cref="HttpStandardHedgingResilienceOptions"/> for more details about the used resilience strategies.
    /// </para>
    /// </remarks>
    public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHttpClientBuilder builder, Action<IRoutingStrategyBuilder> configure)
    {
        _ = Throw.IfNull(builder);
        _ = Throw.IfNull(configure);

        var hedgingBuilder = builder.AddStandardHedgingHandler();

        configure(hedgingBuilder.RoutingStrategyBuilder);

        return hedgingBuilder;
    }

    /// <summary>
    /// Adds a standard hedging handler that wraps the execution of the request with a standard hedging mechanism.
    /// </summary>
    /// <param name="builder">The HTTP client builder.</param>
    /// <returns>
    /// A <see cref="IStandardHedgingHandlerBuilder"/> instance that can be used to configure the standard hedging behavior.
    /// </returns>
    /// <remarks>
    /// The standard hedging uses a pool of circuit breakers to ensure that unhealthy endpoints are not hedged against.
    /// By default, the selection from pool is based on the URL Authority (scheme + host + port).
    /// It is recommended that you configure the way the strategies are selected by calling
    /// <see cref="StandardHedgingHandlerBuilderExtensions.SelectPipelineByAuthority(IStandardHedgingHandlerBuilder)"/>
    /// extensions.
    /// <para>
    /// See <see cref="HttpStandardHedgingResilienceOptions"/> for more details about the used resilience strategies.
    /// </para>
    /// </remarks>
    public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHttpClientBuilder builder)
    {
        _ = Throw.IfNull(builder);

        var optionsName = builder.Name;
        var routingBuilder = new RoutingStrategyBuilder(builder.Name, builder.Services);

        builder.Services.TryAddSingleton<Randomizer>();

        _ = builder.Services.AddOptionsWithValidateOnStart<HttpStandardHedgingResilienceOptions, HttpStandardHedgingResilienceOptionsValidator>(optionsName);
        _ = builder.Services.AddOptionsWithValidateOnStart<HttpStandardHedgingResilienceOptions, HttpStandardHedgingResilienceOptionsCustomValidator>(optionsName);
        _ = builder.Services.PostConfigure<HttpStandardHedgingResilienceOptions>(optionsName, options =>
        {
            options.Hedging.ActionGenerator = args =>
            {
                if (!args.PrimaryContext.Properties.TryGetValue(ResilienceKeys.RequestSnapshot, out var snapshot))
                {
                    Throw.InvalidOperationException("Request message snapshot is not attached to the resilience context.");
                }

                var requestMessage = snapshot.CreateRequestMessage();

                // The secondary request message should use the action resilience context
                requestMessage.SetResilienceContext(args.ActionContext);

                // replace the request message
                args.ActionContext.SetRequestMessage(requestMessage);

                if (args.PrimaryContext.Properties.TryGetValue(ResilienceKeys.RoutingStrategy, out var routingPipeline))
                {
                    if (!routingPipeline.TryGetNextRoute(out var route))
                    {
                        // no routes left, stop hedging
                        return null;
                    }

                    requestMessage.RequestUri = requestMessage.RequestUri!.ReplaceHost(route);
                }

                return () => args.Callback(args.ActionContext);
            };
        });

        // configure outer handler
        var outerHandler = builder.AddResilienceHandler(HedgingConstants.HandlerPostfix, (builder, context) =>
        {
            var options = context.GetOptions<HttpStandardHedgingResilienceOptions>(optionsName);
            context.EnableReloads<HttpStandardHedgingResilienceOptions>(optionsName);
            var routingOptions = context.GetOptions<RequestRoutingOptions>(routingBuilder.Name);

            _ = builder
                .AddStrategy(_ => new RoutingResilienceStrategy(routingOptions.RoutingStrategyProvider))
                .AddStrategy(_ => new RequestMessageSnapshotStrategy())
                .AddTimeout(options.TotalRequestTimeout)
                .AddHedging(options.Hedging);
        });

        // configure inner handler
        var innerBuilder = builder.AddResilienceHandler(
            HedgingConstants.InnerHandlerPostfix,
            (builder, context) =>
            {
                var options = context.GetOptions<HttpStandardHedgingResilienceOptions>(optionsName);
                context.EnableReloads<HttpStandardHedgingResilienceOptions>(optionsName);

                _ = builder
                    .AddRateLimiter(options.Endpoint.RateLimiter)
                    .AddCircuitBreaker(options.Endpoint.CircuitBreaker)
                    .AddTimeout(options.Endpoint.Timeout);
            })
            .SelectPipelineByAuthority();

        // Disable the HttpClient timeout to allow the timeout strategies to control the timeout.
        _ = builder.ConfigureHttpClient(client => client.Timeout = Timeout.InfiniteTimeSpan);

        return new StandardHedgingHandlerBuilder(builder.Name, builder.Services, routingBuilder);
    }

    private sealed record StandardHedgingHandlerBuilder(
        string Name,
        IServiceCollection Services,
        IRoutingStrategyBuilder RoutingStrategyBuilder) : IStandardHedgingHandlerBuilder;
}
